package globox_server;

import java.io.EOFException;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

import rmi.globox.GloboxController;

import app_srv_interface.IGlobox;

import utils.Purchase;
import utils.Reservation;
import utils.Sit;
import utils.Theater;
import utils.jn.State;
import utils.jn.TheaterSit;

public class GloboxServer implements IGlobox {

	private static final int NUM_LIMIT_CLIENTS = 1000000;

	/**
	 * this file defines the properties used in the program : port for
	 * connections with clients, timeout for reservation validation, etc
	 */
	private String configFile = "config/globox.properties";

	private String portClient = null;

	// default 10 seconds, but value may be set in config file
	private int reservCheckTimeout = 10;

	private final int numTotalPlaces = 26 * 40;

	private ServerSocket ss;

	public boolean isCommStartClient;

	private Thread newServerThreadClient;

	private Map<String, Purchase> allPurchases = new HashMap<String, Purchase>();

	private Map<String, Reservation> allReservations = new HashMap<String, Reservation>();

	private String clientIP;

	private Set<Theater> allTheaters = new HashSet<Theater>();

	// key is clientID_thName
	private Map<String, Integer> clientHitCountByTheater = new HashMap<String, Integer>();

	/**
	 * Keeps all client IDs associated with sits that are reserved or occupied.
	 * */
	private Map<Integer, List<Sit>> SitsTID = new HashMap<Integer, List<Sit>>();
	/**
	 * Each client will have a random id between 1 and 1M
	 * */
	private Map<Integer, String> clientIDs = new HashMap<Integer, String>();

	private GloboxController rmiController;

	private String dbRMIHost = "localhost"; // default values, but configurable
											// in properties file

	private int dbRMIPort = 9876;// default values, but configurable in
									// properties file

	private String objName = "BoxOffice";// default values, but configurable in
											// properties file

	// identifies if a client is entering site for the first time
	private boolean firstTimeVisiting = true;

	/**
	 * The constructor
	 * 
	 */
	public GloboxServer() {

		try {
			Properties p = new Properties();
			Reader reader = new FileReader(configFile);

			p.load(reader);
			// initialize fields
			portClient = p.getProperty("port");
			try {
				reservCheckTimeout = Integer.parseInt(p
						.getProperty("reservationValidationTimeout"));
			} catch (Exception e) {
				e.printStackTrace();
			}
			dbRMIHost = p.getProperty("rmi.server.host");
			try {
				dbRMIPort = Integer.parseInt(p.getProperty("rmi.server.port"));
			} catch (Exception e) {
				e.printStackTrace();
			}
			objName = p.getProperty("rmi.server.objectname");

			rmiController = new GloboxController(dbRMIHost, dbRMIPort, objName);

		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		try {
			newServerThreadClient = new EchoServiceClient(
					Integer.parseInt(portClient));
		} catch (Exception e) {
			System.err.println("Couldn't start TCP echo service for client: "
					+ e);
		}
	}

	/**
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws Exception {

		GloboxServer ep = new GloboxServer();
		ep.startServer();
	}

	/**
	 * verify if the user entered the right password
	 */
	public void login() throws IOException {
	}

	/** metodo para arrancar o servidor,usando threads para multiplas conexoes */
	public void startServer() {
		// loadVotesFromFile();
		try {
			newServerThreadClient.start();
		} catch (Exception e) {
			System.out.println("Couldn't start server. Reason: " + e);
		}
		return;
	}

	private static void usage(String[] args) {
		// TODO Auto-generated method stub
		if (args.length != 1) {
			System.out.printf("Usage : java ElectionServer <configfile> \n\n");
			System.exit(1);
		}
	}

	class EchoServiceClient extends Thread {

		public EchoServiceClient(int port) throws IOException {
			// TODO Auto-generated constructor stub
			super("Client Service");
			ss = new ServerSocket(port);
		}

		public void run() {
			Socket cs;
			while (true) {
				try {
					cs = ss.accept();
					(new ServerThread(cs)).start();
				} catch (IOException ie) {
					break;
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}

	}

	class ServerThread extends Thread {

		// private static final String LOGIN = "test";

		private static final String ADD_RESERVATION = app_srv_interface.IGlobox.ADD_RESERVATION;

		private static final String REMOVE_RESERVATION = app_srv_interface.IGlobox.REMOVE_RESERVATION;

		private static final String LIST_RESERVATIONS = app_srv_interface.IGlobox.LIST_RESERVATIONS;

		private static final String UPDATE_RESERVATION = app_srv_interface.IGlobox.UPDATE_RESERVATION;

		private static final String LIST_THEATERS = app_srv_interface.IGlobox.LIST_THEATERS;

		private static final String BROWSE_SITS_THEATER = app_srv_interface.IGlobox.BROWSE_SITS_THEATER;

		private static final String OK = app_srv_interface.IGlobox.OK;

		private static final String NOK = app_srv_interface.IGlobox.NOK;

		private Socket socket = null;

		private ObjectInputStream ois;

		private ObjectOutputStream oos;

		String[] toRcv = null;

		String stuffFromClient;

		private String portClient;

		ServerThread(Socket inSoc) {

			socket = inSoc;
			this.portClient = GloboxServer.this.portClient;
			clientIP = socket.getInetAddress().getHostAddress();
			// if (!clientIDs.containsValue(clientIP)) {
			// // generate a random number between 1 and 1M
			// int clientID = -1;
			// while (true) {
			// clientID = new Random().nextInt(NUM_LIMIT_CLIENTS);
			// if (clientIDs.containsKey(clientID))
			// continue;
			// else {
			// clientIDs.put(clientID, clientIP);
			// break;
			// }
			// }
			// }
		}

		public void run() {
			try {
				ois = new ObjectInputStream(socket.getInputStream());
				oos = new ObjectOutputStream(socket.getOutputStream());

				for (;;) {
					// wait for message
					try {
						readWriteFromSocket();
					} catch (SocketException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
						try {
							ois.close();
							oos.close();
							socket.close();
						} catch (Exception ex) {
						}
					} catch (EOFException e) {
						System.out.println("Client disconnected");
						try {
							socket.close();
						} catch (Exception ex) {
						}
						break;
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
						try {
							socket.close();
						} catch (Exception ex) {
						}
					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
						try {
							socket.close();
						} catch (Exception ex) {
						}
					}
				}// FOR INFINITO
			} catch (SocketException se) {
				System.out.println(se.getMessage());
				try {
					socket.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} catch (EOFException e) {
				return;
			} catch (IOException e) {
				e.printStackTrace();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} finally {
				try {
					ois.close();
					oos.close();
					// inStreamManager.close();
					// outStreamManager.close();
					socket.close();
					// manSock.close();
				} catch (IOException e) {

				}
			}
		}

		/**
		 * waiting for a message from client,rcv and treat it right
		 * 
		 * @throws ClassNotFoundException
		 */
		public synchronized void readWriteFromSocket() throws IOException,
				ClassNotFoundException {
			String stuffFromClient = "";
			// TODO Auto-generated method stub
			if (socket.getLocalPort() == Integer.parseInt(this.portClient)
					&& socket.isConnected()) {

				stuffFromClient = (String) ois.readObject();

				System.out.println("stuffFromClient = " + stuffFromClient);

				if (stuffFromClient.equals(ADD_RESERVATION)) {
					try {
						System.out.println("Op=" + ADD_RESERVATION);
						// Reservation res = (Reservation) ois.readObject();
						Integer clientID = (Integer) ois.readObject();
						String theater = (String) ois.readObject();
						@SuppressWarnings("unchecked")
						List<Sit> sits = (List<Sit>) ois.readObject();

						if (theater == null || theater.equalsIgnoreCase("")
								|| sits == null || sits.size() == 0) {
							oos.writeObject(NOK);
							oos.flush();
							return;
						}

						List<Sit> map = null;

						synchronized (GloboxServer.this) {
							// reuse the same object
							map = addReservation(clientID, theater, sits);
						}
						oos.writeObject(map);
						oos.flush();
					} catch (Exception e) {
						e.printStackTrace();
						oos.writeObject(NOK);
						oos.flush();
					}
				} else if (stuffFromClient.equals(ADD_PURCHASE)) {
					try {
						System.out.println("Op=" + ADD_PURCHASE);
						// UUID key = (UUID) ois.readObject();
						// synchronized (GloboxServer.this) {
						// removeReservation(key);
						// }

						Integer clientID = (Integer) ois.readObject();
						// String theater = (String) ois.readObject();
						// @SuppressWarnings("unchecked")
						// List<Sit> sits = (List<Sit>) ois.readObject();
						//
						// if (theater == null || theater.equalsIgnoreCase("")
						// || sits == null || sits.size() == 0) {
						// oos.writeObject(NOK);
						// oos.flush();
						// return;
						// }

						synchronized (GloboxServer.this) {
							addPurchase(clientID);
						}
						oos.writeObject(OK);
						oos.flush();
						oos.writeObject(OK);
						oos.flush();
					} catch (Exception e) {
						e.printStackTrace();
						oos.writeObject(NOK);
						oos.flush();
					}
				} else if (stuffFromClient.equals(LIST_RESERVATIONS)) {
					try {
						System.out.println("Op=" + LIST_RESERVATIONS);
						synchronized (GloboxServer.this) {
							List<Reservation> reservs = listReservations();
							oos.writeObject(reservs);
							oos.flush();
						}
					} catch (Exception e) {
						e.printStackTrace();
						oos.writeObject(NOK);
						oos.flush();
					}
				} else if (stuffFromClient.equals(UPDATE_RESERVATION)) {
					try {
						System.out.println("Op=" + UPDATE_RESERVATION);
						UUID key = (UUID) ois.readObject();
						Reservation res = (Reservation) ois.readObject();
						synchronized (GloboxServer.this) {
							updateReservation(key, res);
						}
						oos.writeObject(OK);
						oos.flush();
					} catch (Exception e) {
						e.printStackTrace();
						oos.writeObject(NOK);
						oos.flush();
					}
				} else if (stuffFromClient.equals(LIST_THEATERS)) {
					try {

						System.out.println("Op=" + LIST_THEATERS);
						String query = (String) ois.readObject();

						System.out.println("getting list");

						Collection<Theater> rslt = listTheaters(query);

						System.out.println("before writeing response");

						oos.writeObject(rslt);
						oos.flush();

					} catch (Exception e) {
						e.printStackTrace();
						oos.writeObject(NOK);
						oos.flush();
					}
				} else if (stuffFromClient.equals(BROWSE_SITS_THEATER)) {
					try {
						System.out.println("Op=" + BROWSE_SITS_THEATER);

						Integer clientID = (Integer) ois.readObject();
						String thName = (String) ois.readObject();

						if (clientHitCountByTheater == null)
							System.out
									.println("clientHitCountByTheater is NULL");

						// check if client is new
						if (clientHitCountByTheater.containsKey(clientID + "_"
								+ thName)) {
							firstTimeVisiting = false;
							int count = clientHitCountByTheater.get(clientID
									+ "_" + thName);
							clientHitCountByTheater.put(
									clientID + "_" + thName, (count + 1));
						} else {

							clientHitCountByTheater.put(
									clientID + "_" + thName, 1);
							firstTimeVisiting = true;
						}

						System.out.println("getting list");

						Collection<Sit> rslt = browseSitsByTheater(thName,
								clientID);
						if (rslt != null) {
							System.out
									.println("before writeing response, found ("
											+ rslt.size() + ") sits");

							oos.writeObject(rslt);
							oos.flush();
						} else {
							oos.writeObject(NOK);
							oos.flush();
						}
					} catch (Exception e) {
						e.printStackTrace();
						oos.writeObject(NOK);
						oos.flush();
					}
				} else {
					oos.writeObject(NOK);
					oos.flush();
					return;
				}
			}
		}
	}

	@Override
	public Collection<Theater> listTheaters(String pattern) {

		// System.out.println("method listTheaters, before getting stuff");
		// hammer1();
		// System.out.println("method listTheaters, before returning");

		Collection<Theater> rslt = new ArrayList<Theater>();

		try {
			Iterable<String> cinemas = rmiController.GetTheaters();

			for (String name : cinemas) {
				Theater t = new Theater(name, numTotalPlaces, "PT", "Lisbon");
				allTheaters.add(t);
			}

			for (Theater t : allTheaters)
				if (t.getName().toLowerCase().contains(pattern.toLowerCase())
						&& !rslt.contains(t))
					rslt.add(t);
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (NotBoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}

		return rslt;
	}

	@Override
	public void updateReservation(UUID key, Reservation r) throws Exception {
		allReservations.put(key.toString(), r);
	}

	@Override
	public List<Purchase> listPurchases() {
		List<Purchase> purchases = new ArrayList<Purchase>();
		for (Purchase ev : allPurchases.values())
			purchases.add(ev);
		return purchases;
	}

	@Override
	public List<Reservation> listReservations() {
		// TODO Auto-generated method stub
		List<Reservation> reservation = new ArrayList<Reservation>();
		for (Reservation ev : allReservations.values())
			reservation.add(ev);
		return reservation;
	}

	@Override
	public void addPurchase(Purchase p) {
		UUID key = null;
		while (!allPurchases.containsKey(key)) {
			key = UUID.randomUUID();
			allPurchases.put(key.toString(), p);
		}
		// set id for added purchase
		p.setId(key);
	}

	public String addPurchase(Integer clientID) {
		// send to db server
		// transform to utils.jn.Sit
		// List<utils.jn.Sit> jnSits = new ArrayList<utils.jn.Sit>();
		// for (Sit s : sits) {
		// utils.jn.Sit jnsit = new TheaterSit(Integer.parseInt(s.getNumber()
		// .split("-")[0]),
		// Integer.parseInt(s.getNumber().split("-")[1]));
		// jnSits.add(jnsit);
		// }

		// Integer transactionID = getKeyByValue(clientIDs, clientIP);
		// if (transactionID == null) {
		// System.out.println("transactionID NOT FOUND in memory");
		// return NOK;
		// }
		try {
			boolean rslt = rmiController.addPurchase(clientID);
			if (!rslt)
				return NOK;
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return NOK;
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return NOK;
		} catch (NotBoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return NOK;
		}

		return OK;
	}

	@Override
	public void addReservation(Reservation r) {
		//
		UUID key = null;
		while (!allReservations.containsKey(key)) {
			key = UUID.randomUUID();
			allReservations.put(key.toString(), r);
		}
	}

	public List<Sit> addReservation(Integer clientID, String theater,
			List<Sit> sits) {

		List<utils.Sit> mySits = new ArrayList<utils.Sit>();
		// check if client is new
		// if (clientIDs.containsKey(clientID))
		// firstTimeVisiting = false;
		// else {
		// clientIDs.put(clientID, clientIP);
		// firstTimeVisiting = true;
		// }

		// send to db server
		// transform to utils.jn.Sit
		List<utils.jn.Sit> jnSits = new ArrayList<utils.jn.Sit>();
		for (Sit s : sits) {
			utils.jn.Sit jnsit = new TheaterSit(Integer.parseInt(s.getNumber()
					.split("-")[0]),
					Integer.parseInt(s.getNumber().split("-")[1]));
			jnSits.add(jnsit);
		}

		System.out.println("New reservation with transaction ID:" + clientID);

		try {
			Iterable<utils.jn.Sit> rslt = rmiController.makeReservation(
					theater, clientID, jnSits);
			// transform back to utils.Sit
			for (utils.jn.Sit s : rslt) {
				utils.Sit sit = new Sit(s.getRow() + "-" + s.getColumn());
				if (s.state() == State.RESERVED) {
					sit.setReserved(true);
					sit.setOccupiedBy(clientID + "");
				}
				if (s.state() == State.OCCUPIED) {
					sit.setOcupied(true);
//					sit.setOccupiedBy(clientID + "");
				}
				mySits.add(sit);
			}
			// keep track of reserved-sits/clientID
			SitsTID.put(clientID, mySits);
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (NotBoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
		// startBackgroundTimerTask(clientID, theater, sits);
		return mySits;
	}

	/**
	 * Reserving seats that are already reserved, frees the respective seats
	 * */
	private void startBackgroundTimerTask(final Integer clientID,
			final String theater, final List<Sit> sits) {
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(reservCheckTimeout * 1000);
					// tell db server to free the seats
					List<utils.jn.Sit> toBeFreed = new ArrayList<utils.jn.Sit>();
					for (Sit s : sits) {
						utils.jn.Sit ts = new utils.jn.TheaterSit(Integer
								.parseInt(s.getNumber().split("-")[0]), Integer
								.parseInt(s.getNumber().split("-")[1]));
						toBeFreed.add(ts);
					}

					System.out
							.println("Background thread will free uncofirmed reserved sits for theater "
									+ theater);
					try {
						rmiController.makeReservation(theater, clientID,
								toBeFreed);
						System.out.println("Seats freed");
					} catch (RemoteException e) {
						System.out
								.println("Seats not freed or freed with errors!");
						e.printStackTrace();
					} catch (MalformedURLException e) {
						System.out
								.println("Seats not freed or freed with errors!");
						e.printStackTrace();
					} catch (NotBoundException e) {
						System.out
								.println("Seats not freed or freed with errors!");
						e.printStackTrace();
					}
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		});
		t.start();
	}

	@Override
	public void removeReservation(UUID key) {
		//
		allReservations.remove(key);
	}

	// hammer tests
	private void hammer1() {
		allTheaters.clear();
		for (int i = 0; i < 15; i++) {
			Theater t = new Theater("Theater_" + i, 64, "Portugal", "Lisbon");
			System.out.println("method listTheaters, created theater object");
			// UUID id = UUID.randomUUID();
			allTheaters.add(t);
			System.out.println("method listTheaters, added t to list");
		}

	}

	@Override
	public Collection<Sit> browseSitsByTheater(String theaterName, Integer cid) {
		// generate a random number between 1 and 1M
		// int transactionID = getKeyByValue(clientIDs, clientIP);
		// connect to db and make a reservation
		try {
			List<Sit> mysits = new ArrayList<Sit>();
			List<Sit> myReservedsits = new ArrayList<Sit>();

			Iterable<utils.jn.Sit> sits = null;

			if (firstTimeVisiting)
				sits = rmiController.makeReservation(theaterName, cid);
			else
				sits = rmiController.getSeatsByTheater(theaterName);

			for (utils.jn.Sit jnsit : sits) {
				Sit s = new Sit(jnsit.getRow() + "-" + jnsit.getColumn());

				if (jnsit.state() == State.OCCUPIED) {
					s.setOcupied(true);
					s.setOccupiedBy(cid + "");
				}
				if (jnsit.state() == State.RESERVED) {
					s.setReserved(true);
					myReservedsits.add(s);
					s.setOccupiedBy(cid + "");
				}
				mysits.add(s);
			}

			// keep track of reserved-sits/transactionID
			SitsTID.put(cid, myReservedsits);

			for (Theater t : allTheaters)
				if (t.getName().toLowerCase().equalsIgnoreCase(theaterName)) {
					t.setAllSits(mysits);
					return t.getAllSits();
				}
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NotBoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return null;
	}

	public static <T, E> T getKeyByValue(Map<T, E> map, E value) {
		for (Entry<T, E> entry : map.entrySet()) {
			if (value.equals(entry.getValue())) {
				return entry.getKey();
			}
		}
		return null;
	}
}
